package net.neocat.tcx.util;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import net.neocat.tcx.file.GeoPosition;

import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.concat;
import static net.neocat.tcx.util.RamerDouglasPeuckerFilter.simplifyLine2D;

/**
 * Created with IntelliJ IDEA.
 * User: jschek
 * Date: 4/29/13
 * Time: 9:15 PM
 * To change this template use File | Settings | File Templates.
 */
public class LineSegment {
    private final ImmutableList<GeoPosition> vertexes;
    private final GeoPosition interpolatedPoint;
    private final ImmutableMap<String,Object> tags;

    public LineSegment(List<GeoPosition> vertexes) {
        this(vertexes, ImmutableMap.<String, Object>of());
    }

    public LineSegment(List<GeoPosition> vertexes, Map<String,? extends Object> tags) {
        this.vertexes = ImmutableList.copyOf(checkNotNull(vertexes, "vertexes"));
        this.interpolatedPoint = null;
        checkArgument(vertexes.size() >= 2, "vertexes must have at least 2 points");
        this.tags = ImmutableMap.copyOf(checkNotNull(tags,"tags"));
    }

    @VisibleForTesting
    protected LineSegment(List<GeoPosition> vertexes, GeoPosition interpolatedPoint) {
        this(vertexes, interpolatedPoint, ImmutableMap.<String, Object>of());
    }

    @VisibleForTesting
    protected LineSegment(List<GeoPosition> vertexes, GeoPosition interpolatedPoint, ImmutableMap<String,Object> tags) {
        this.vertexes = ImmutableList.copyOf(checkNotNull(vertexes, "vertexes"));
        checkArgument(vertexes.size() >= 2, "vertexes must have at least 2 points");
        this.interpolatedPoint = interpolatedPoint;
        this.tags = checkNotNull(tags,"tags");
    }

    public static GeoPosition interpolatePointsPercent(GeoPosition start, GeoPosition end, double percent) {
        checkNotNull(start, "start");
        checkNotNull(start, "end");
        checkArgument(0<=percent && percent<=1.0, "percent must be [0,1]", percent);
        return interpolatePointsMeters(start, end, start.greatCircleDistanceMeters2D(end) * percent);
    }

    public static GeoPosition interpolatePointsMeters(GeoPosition start, GeoPosition end, double targetDistance) {
        checkNotNull(start, "start");
        checkNotNull(start, "end");
        checkArgument(targetDistance >= 0.0, "targetDistance must be non-negative");
        final double totalDistance = start.greatCircleDistanceMeters2D(end);
        if (targetDistance == 0) {
            return start;
        }
        if (targetDistance >= totalDistance) {
            return end;
        }
        final double ratio = targetDistance / totalDistance;
        double dx = (end.getX() - start.getX()) * ratio;
        double dy = (end.getY() - start.getY()) * ratio;
        double dz = (end.getZ() - start.getZ()) * ratio;
        return start.plus(dx, dy, dz);
    }

    public double getGreatCircleDistanceMeters2D() {
        double distance = 0;
        GeoPosition previous = vertexes.get(0);
        for (GeoPosition current : vertexes.subList(1, vertexes.size())) {
            distance += previous.greatCircleDistanceMeters2D(current);
            previous = current;
        }
        return distance;
    }

    public LineSegment insertPointAlongLine(double metersFromStart) {
        checkArgument(metersFromStart >= 0.0, "metersFromStart must be non-negative");
        if(metersFromStart==0) {
            return this;
        }
        List<GeoPosition> vertexesWithNewPoint = Lists.newArrayList(vertexes);
        double totalDistance = 0;
        GeoPosition previous = vertexes.get(0);
        for (ListIterator<GeoPosition> iterator = vertexesWithNewPoint.listIterator(1); iterator.hasNext(); ) {
            GeoPosition current = iterator.next();
            double currentDistance = previous.greatCircleDistanceMeters2D(current);
            if ((totalDistance + currentDistance) > metersFromStart) {
                GeoPosition newPoint = interpolatePointsMeters(previous, current, metersFromStart - totalDistance);
                iterator.previous();
                iterator.add(newPoint);
                return new LineSegment(vertexesWithNewPoint,newPoint, tags);
            }
            totalDistance += currentDistance;
            previous = current;
        }
        return this;
    }

    public LineSegment generalize(double toleranceInMeters) {
        if(vertexes.size()<=2) {
            return this;
        }

        double toleranceInDegrees = GeoUtils.approximateDegrees(getFirst(),toleranceInMeters);
        if(interpolatedPoint==null) {
            return new LineSegment(simplifyLine2D(toleranceInDegrees, vertexes), tags);
        } else {
            int index = vertexes.indexOf(interpolatedPoint);
            if(index<0) {
                return new LineSegment(simplifyLine2D(toleranceInDegrees, vertexes), tags);
            }
            List<GeoPosition> firstSegment = simplifyLine2D(toleranceInDegrees,vertexes.subList(0,index+1));
            List<GeoPosition> secondSegment= simplifyLine2D(toleranceInDegrees,vertexes.subList(index,vertexes.size()));
            List<GeoPosition> combined = ImmutableList.copyOf(concat(firstSegment, secondSegment.subList(1,secondSegment.size())));
            return new LineSegment(combined,interpolatedPoint, tags);
        }
    }

    public int getSize() {
        return vertexes.size();
    }

    public GeoPosition getFirst() {
        return vertexes.get(0);
    }

    public GeoPosition getLast() {
        return vertexes.get(vertexes.size() - 1);
    }

    public ImmutableList<GeoPosition> getVertexes() {
        return vertexes;
    }

    public GeoPosition getInterpolatedPoint() {
        return interpolatedPoint;
    }

    public ImmutableMap<String, Object> getTags() {
        return tags;
    }
}

